import fg from "fast-glob";
import fs from "fs-extra";
import path from "path";
import _ from "lodash";

declare type TLiteral = number | string;

declare type IOptimizeMeta<T> = {
    meta: Partial<T>,
    arrayFields?: (keyof T)[],
    complexFields?: IComplexOptimizeMeta<T>
}

declare type IComplexOptimizeMeta<T> = {
    [P in keyof T]: IOptimizeMeta<Exclude<T[keyof T], TLiteral>>
}

interface IOptimizeOut<T> extends IOptimizeMeta<T> {
    data: Partial<T>[]
}

interface IVCounter<T> {
    value: T[keyof T],
    count: number
}

/**
 * @deprecated 无内存优化效果，弃用
 */
export class JsonOptimizer {
    /**
     * 压缩JSON，合并相同字段
     * @param objs 
     * @returns 
     */
    public static optimize<T>(objs: T[]): IOptimizeOut<T> {
        const om = JsonOptimizer.getMetaObject(objs);
        const data: Partial<T>[] = [];
        for (const obj of objs) {
            data.push(JsonOptimizer.pruneObject(obj, om));
        }
        return { ...om, data };
    }

    /**
     * 还原JSON，通过proxy
     * @param oo 
     * @returns 
     */
    public static restore<T>(oo: IOptimizeOut<T>): T[] {
        return this.restoreObject(oo.data, oo, true);
    }

    private static getMetaObject<T>(objs: T[]): IOptimizeMeta<T> {
        // 先收集公共key
        const keysArr: (keyof T)[][] = [];
        for (const o of objs) {
            const keys = Object.keys(o as any) as (keyof T)[];
            keysArr.push(keys);
        }
        const commonKeys = _.intersection(...keysArr);

        // 统计所有字段的高频值
        const meta: any = {}, arrayFields: (keyof T)[] = [], complexFields: IComplexOptimizeMeta<T> = {} as IComplexOptimizeMeta<T>;
        for (const key of commonKeys) {
            let isArrayField = false, isComplex = false;
            if (objs[0][key] instanceof Array) {
                meta[key] = [];
                isArrayField = true;
                arrayFields.push(key);

                // 数组的话需要遍历才能确定是否复杂结构
                for (const o of objs) {
                    const value = o[key] as any[];
                    for (const sv of value) {
                        if (typeof(sv) == 'object') {
                            isComplex = true;
                            break;
                        }
                    }
                    if (isComplex) break;
                }
            } else {
                isComplex = typeof(objs[0][key]) == 'object';
            }

            if (isComplex) {
                // 复杂结构抽取公共字段
                const values: any[] = [];
                for (const o of objs) {
                    const value = o[key];
                    if (value instanceof Array) {
                        values.push(...value);
                    } else {
                        values.push(value);
                    }
                }
                const om = JsonOptimizer.getMetaObject<Exclude<T[keyof T], TLiteral>>(values);
                complexFields[key] = om;
            }

            const frequencyCnt: { [value: string]: IVCounter<T> } = {};
            for (const o of objs) {
                const value = o[key];
                const valueKey = isArrayField || isComplex ? JSON.stringify(value) : (value as TLiteral);
                let counter = frequencyCnt[valueKey];
                if (counter != null) {
                    counter.count += 1;
                } else {
                    frequencyCnt[valueKey] = counter = { value, count: 1 };
                }
            }
            let defaultCounter: IVCounter<T> | null = null;
            for (const value in frequencyCnt) {
                const counter = frequencyCnt[value];
                if (defaultCounter == null || defaultCounter.count < counter.count) {
                    defaultCounter = counter;
                }
            }
            if (Object.keys(frequencyCnt).length < objs.length) {
                meta[key] = defaultCounter!.value;
            } else {
                console.log('field not shared in common:', key);
            }
        }
        return { meta, arrayFields, complexFields };
    }

    private static pruneObject<T>(obj: T, om: IOptimizeMeta<T>): Partial<T> {
        const o: Partial<T> = {};
        for (const key in obj) {
            const value = obj[key];
            if (key in om.meta) {
                // 有默认值
                if (!_.isEqual(value, om.meta[key])) {
                    // 与默认值不同
                    const subMeta = om.complexFields?.[key];
                    if (subMeta != null) {
                        // 复杂结构进一步剥离字段
                        if (value instanceof Array) {
                            const newValue = [];
                            for (const sv of value) {
                                newValue.push(JsonOptimizer.pruneObject(sv as Extract<Exclude<T[keyof T], TLiteral>, T>, subMeta));
                            }
                            o[key] = newValue as any;
                        } else {
                            o[key] = JsonOptimizer.pruneObject(value as any, subMeta) as any;
                        }
                    } else {
                        o[key] = value;
                    }
                }
            } else {
                // 没默认值
                o[key] = value;
            }
        }
        return o;
    }

    private static restoreObject(o: any, patch: IOptimizeMeta<any>, isArray: boolean): any {
        if (isArray) {
            for (let i = o.length - 1; i >= 0; i--) {
                o[i] = JsonOptimizer.restoreObject(o[i], patch, false);
            }
            return o;
        } else {
            const proxy = new Proxy(o, {
                get(target, p, receiver) {
                    if (p in target) {
                        return target[p];
                    }
                    return patch.meta[p as string];
                }
            });
            if (patch.complexFields != null) {
                for (const k in patch.complexFields) {
                    o[k] = JsonOptimizer.restoreObject(proxy[k], patch.complexFields[k], Boolean(patch.arrayFields?.includes(k)));
                }
            }
            return proxy;
        }
    }
}

export async function test(): Promise<void> {
    const root = 'D:/works/fy-convertor/temp/mu_mobile/json';
    const out = path.join(root, '../optimizedJson');
    await fs.ensureDir(out);
    const jsons = await fg('*.json', { cwd: root });
    // console.log(jsons.map((v)=>`'${v}'`).join(', '));
    for (const json of jsons) {
        const objs = await fs.readJSON(path.join(root, json));
        const om = JsonOptimizer.optimize(objs);
        await fs.writeJSON(path.join(out, json), om, { spaces: 2 });
    }
}